<?php
/**
 * This file is part of League of Legends PHP Inibin Parser.
 *
 * League of Legends PHP Inibin Parser is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 *
 * League of Legends PHP Inibin Parser is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with League of Legends PHP Inibin Parser.  If not, see <http://www.gnu.org/licenses/>.
 */

/**
 * League of Legends Client-Database parser
 * Class: parser
 * 
 * Based on the work of ItzWarty, Engberg and James Lammlein.
 *
 * @author Marcel Klimek <klimek@secondred.de>
 * @copyright Marcel Klimek
 * @version $Id: class.inibin.parser.php 3 2011-11-12 10:07:44Z naffto88@gmail.com $
 */
class parser
{
    /**
     * Debug-Mode
     * @var boolean
     */
    private $bDebug = false;

    /**
     * Storage for the binary data of the current file
     * @var byte(string)
     * @access private
     */
    private $binfile = null;

    /**
     * The whole filesize
     * @var integer
     * @access private
     */
    private $iFilesize = 0;

    /**
     * The binaray data stored in an array as Hex-Values
     * @var array
     * @access private
     */
    private $hexFile = array();

    /**
     * The binaray data stored in an array as Decimal Values
     * @var array
     * @access private
     */
    private $decFile = array();

    /**
     * Array of Key - Value Pairs
     * @var array
     * @access private
     */
    private $arrKeyValue = array();

    /**
     * The name of the current file
     * @var string
     * @access private
     */
    private $strFilename = "";

    /**
     * Just the Pointer within the byte-array
     * @var integer
     * @access private
     */
    private $iStreamPointer = 0;


    /**
     * Returns the Value which corresponds to the Key
     *
     * @param String $strKey
     * @return mixed String, Long, Int
     * @access public
     */
    public function getValue($strKey)
    {
        if (array_key_exists((string) $strKey, $this->arrKeyValue))
        {
            return $this->arrKeyValue[(string) $strKey];
        } else
        {
            return 0;
        }
    }


    /**
     * This class just resets all class values for the next file
     *
     * @access private
     */
    private function resetClassVars()
    {
        $this->binfile = "";
        $this->arrKeyValue = array();
        $this->decFile = array();
        $this->hexFile = array();
        $this->iFilesize = 0;
        $this->iStreamPointer = 0;
    }


    /**
     * Loads the File and parse all Values and Keys
     *
     * @param String $strFilePath
     * @return boolean False - File doesn't exist
     * @access public
     */
    public function loadFile($strFilePath)
    {
        if (!file_exists($strFilePath))
        {
            return false;
        }
        
        //get the filename
        $arrFilenames = explode("\\",$strFilePath);
        $arrFilenames = explode('.',$arrFilenames[count($arrFilenames)-1]);
        $this->strFilename = $arrFilenames[0];

        //reset all class vars for the new process
        $this->resetClassVars();

        $datei = fopen($strFilePath,"rb");
        $this->iFilesize = filesize($strFilePath);
        $this->binfile = fread($datei,$this->iFilesize);

        //workaround for php
        $strHex = unpack('H*',$this->binfile);
        $this->splitToHexArray($strHex[1]);
        $this->_hex8dec($strHex[1]);

        $this->ReadCharacterInibin();
    }


    /**
     *  This method fill the class var $decFile with the corresponding Decimal values
     *  It will convert the $strHex in 8-bit steps
     *
     * @param String $strHex The full Hex-String of the Binary File
     * @param integer $i    The current Position in the Hex-String
     * @param boolean $unsigned Convert to unsigned Value
     * @access private
     */
    private function _hex8dec($strHex,$i = 0,$unsigned = false)
    {
        $iStart = $i * 2;
        $strHexVal = substr($strHex,$iStart,2);
        $decVal = hexdec($strHexVal);

        if($decVal > 0xFF)
        {
            $this->decFile[$i] = 99999;
        }

        if($decVal >= 0x80 && $unsigned == false)
        {
            $this->decFile[$i] = -(($decVal ^ 0xFF)+1);
        } else
        {
            $this->decFile[$i] = $decVal;
        }

        if ($iStart+2 < strlen($strHex))
        {
            $i++;
            $this->_hex8dec($strHex,$i);
        }
    }


    /**
     * This method will return the Decimal Value for $strHex
     *
     * @param String $strHex An 8-Bit Hex-Value
     * @param boolean $bUnsigned Convert to unsigned Value
     * @access private
     */
    private function hex8dec($strHex,$bUnsigned = false)
    {
        $decVal = hexdec($strHex);

        if($decVal > 0xFF)
        {
            return false;
        }

        if($decVal >= 0x80 && $bUnsigned == false)
        {
            $decVal = -(($decVal ^ 0xFF)+1);
        }

        return $decVal;
    }


    /**
     * This Method splits the raw binaray File into an full Hex-String
     *
     * @param byte(String) $binFile
     * @param integer $i
     * @access private;
     */
    private function splitToHexArray($binFile,$i = 0)
    {
        $iStart = $i * 2;
        $this->hexFile[$i] = substr($binFile,$iStart,2);

        if ($iStart+2 < strlen($binFile))
        {
            $i++;
            $this->splitToHexArray($binFile,$i);
        }
    }


    /**
     * This Method gets the Value from the File at the specific offset
     * This method searches for the next \0x00 Byte to determine the end of an
     * value
     *
     * @param integer,long $strOffset
     * @return mixed Value for the Offset
     * @access private
     */
    private function getData($strOffset)
    {
        $i = $strOffset;
		$strReturn = "";

		while($i < $this->iFilesize)
        {
			if (count($this->decFile) > $i)
            {
				$iASCII = $this->decFile[$i];
            } else
            {
				continue;
            }

			if($iASCII == 0)
            {
				return $strReturn;
			} else
            {
				$strReturn .= (string) chr($iASCII);
			}
            
			$i++;
		}

        return $strReturn;
    }


    /**
     * This is the debug Method for this class
     *
     * @param string $strDebug
     * @access private
     */
    private function debug($strDebug)
    {
        if ($this->bDebug == true)
        {
            echo "[DEBUG] " . $strDebug . "<br/>";
        }
    }


    /**
     * Sets the Debug-Mode
     *
     * @param boolean $bDebug
     * @access public
     */
    public function setDebug($bDebug)
    {
        $this->bDebug = $bDebug;
    }


    /**
     * Returns the name of the current file
     *
     * @return string
     * @access public
     */
    public function getFilename()
    {
        return $this->strFilename;
    }


    /**
     * This Method will Read all Current Segment Keys if there are U32
     * Values in the inibin, defined by $iFormat
     *
     * @param byte $iFormat
     * @access private
     */
    private function ReadU32FromInibin($iFormat)
    {
        if (($iFormat & 0x0001) == 0)
        {
            $this->debug("Kein U32 Wert");
        } else
        {
            // Reads values based on the hash keys.
            $this->debug("U32 Start Position: " . $this->getStreamPointer());

            $arrKeys = $this->ReadSegmentKeys();
            if (is_array($arrKeys))
            {
                foreach ($arrKeys AS $iKey)
                {
                    $iVal = $this->ReadInt32();
                    $this->debug("U32 Value(".$iKey.") " . $iVal);

                    $this->arrKeyValue["".$iKey.""] = $iVal;    //php hack for keys higher than int32
                }
            }
        }
    }


    /**
     * This Method will Read all Current Segment Keys if there are Float
     * Values in the inibin, defined by $iFormat
     *
     * @param byte $iFormat
     * @access private
     */
    private function ReadFloatFromInibin($iFormat)
    {
        if (($iFormat & 0x0002) == 0)
        {
            $this->debug("Kein Float Wert");
        } else
        {
            $this->debug("Float Start Position: " . $this->getStreamPointer());
            $arrKeys = $this->ReadSegmentKeys();
            if (is_array($arrKeys))
            {
                foreach ($arrKeys AS $iKey)
                {
                    $floatVal = $this->ReadFloat();
                    $this->debug("Float Value(".$iKey.") " . $floatVal);
                    $this->arrKeyValue["".$iKey.""] = $floatVal;    //php hack for keys higher than int32
                }
            }
        }
    }


    /**
     * This Method will Read all Current Segment Keys if there are U8/10
     * Values in the inibin, defined by $iFormat
     *
     * @param byte $iFormat
     * @access private
     */
    private function ReadU8D10FromInibin($iFormat)
    {
        if (($iFormat & 0x0004) == 0)
        {
            $this->debug("Kein U8/10 Wert");
        }
        else
        {
            $this->debug("U8/10 Start Position: " . $this->getStreamPointer());
            $arrKeys = $this->ReadSegmentKeys();
            if (is_array($arrKeys))
            {
                foreach ($arrKeys AS $iKey)
                {
                    $floatVal = $this->hex8dec($this->getCurrentByte(true),true) * 0.1;
                    $this->debug("U8/10 Value(".$iKey.") " . $floatVal);
                    $this->arrKeyValue["".$iKey.""] = $floatVal;    //php hack for keys higher than int32
                }
            }
        }
    }


    /**
     * This Method will Read all Current Segment Keys if there are U16
     * Values in the inibin, defined by $iFormat
     *
     * @param byte $iFormat
     * @access private
     */
    private function ReadU16FromInibin($iFormat)
    {
        if (($iFormat & 0x0008) == 0)
        {
            $this->debug("Kein U16 Wert");
        } else
        {
            $this->debug("U16 Start Position: " . $this->getStreamPointer());
            $arrKeys = $this->ReadSegmentKeys();
            if (is_array($arrKeys))
            {
                foreach ($arrKeys AS $iKey)
                {
                    $floatVal = $this->ReadShort();
                    $this->debug("U16 Value(".$iKey.") " . $floatVal);

                    $this->arrKeyValue["".$iKey.""] = $floatVal;    //php hack for keys higher than int32
                }
            }
        }
    }


    /**
     * This Method will Read all Current Segment Keys if there are U8
     * Values in the inibin, defined by $iFormat
     *
     * @param byte $iFormat
     * @access private
     */
    private function ReadU8FromInibin($iFormat)
    {
        if (($iFormat & 0x0010) == 0)
        {
            $this->debug("Kein U8 Wert");
        } else
        {
            $this->debug("U8 Start Position: " . $this->getStreamPointer());
            $arrKeys = $this->ReadSegmentKeys();
            if (is_array($arrKeys))
            {
                foreach ($arrKeys AS $iKey)
                {
                    $floatVal = 0xff & $this->getCurrentByte();
                    $this->debug("U8 Value(".$iKey.") " . $floatVal);

                    $this->arrKeyValue["".$iKey.""] = $floatVal;    //php hack for keys higher than int32
                }
            }
        }
    }


    /**
     * This Method will Read all Current Segment Keys if there are Boolean
     * Values in the inibin, defined by $iFormat
     *
     * @param byte $iFormat
     * @access private
     */
    private function ReadBooleanFromInibin($iFormat)
    {
        if (($iFormat & 0x0020) == 0)
        {
            $this->debug("Kein Boolean Wert");
        } else
        {
            $this->debug("Boolean Start Position: " . $this->getStreamPointer());
            $arrKeys = $this->ReadSegmentKeys();
            if (is_array($arrKeys))
            {
                $iIndex = 0;
                for ($i = 0; $i < ceil((count($arrKeys)) / 8); $i++)
                {
                    $iByte = $this->getCurrentByte();
                    for ($b = 0; $b < 8; $b++)
                    {
                        $iKey = $arrKeys[$iIndex];
                        $bVal = 0x1 & $iByte;
                        $this->debug("Boolean Value(".$iKey.") " . $bVal);

                        $this->arrKeyValue["".$iKey.""] = $bVal;    //php hack for keys higher than int32

                        $iByte = $iByte >> 1;
                        
                        $iIndex++;
                        if ($iIndex >= count($arrKeys))
                        {
                            break;
                        }
                    }
                }
            }
        }
    }
    

    /**
     * This Method will Read all Current Segment Keys if there are 4-Byte Color
     * Values in the inibin, defined by $iFormat
     *
     * @param byte $iFormat
     * @access private
     */
    private function Read4BColorFromInibin($iFormat)
    {
        if (($iFormat & 0x0400) == 0)
        {
            $this->debug("Kein 4-Byte Color Wert");
        } else
        {
            $this->debug("Color Start Position: " . $this->getStreamPointer());
            $arrKeys = $this->ReadSegmentKeys();
            if (is_array($arrKeys))
            {
                foreach ($arrKeys AS $iKey)
                {
                    $iVal = $this->ReadInt32();
                    $this->debug("Color Value(".$iKey.") " . $iVal);

                   $this->arrKeyValue["".$iKey.""] = $iVal;    //php hack for keys higher than int32
                }
            }
        }
    }
    

    /**
     * This Method will Read all Current Segment Keys if there are Rage
     * Values in the inibin, defined by $iFormat
     *
     * @param byte $iFormat
     * @access private
     */
    private function ReadRageFromInibin($iFormat)
    {
        if (($iFormat & 0x0080) == 0)
        {
            $this->debug("Kein Rage Segment");
        } else
        {
            $this->debug("Rage Start Position: " . $this->getStreamPointer());
            $arrKeys = $this->ReadSegmentKeys();
            if (is_array($arrKeys))
            {
                foreach ($arrKeys AS $iKey)
                {
                    $floatVal1 = $this->ReadFloat();
                    $floatVal2 = $this->ReadFloat();
                    $floatVal3 = $this->ReadFloat();

                    $this->debug("Rage Value 1 (".$iKey.") " . $floatVal1);
                    $this->debug("Rage Value 2 (".$iKey.") " . $floatVal2);
                    $this->debug("Rage Value 3 (".$iKey.") " . $floatVal3);

                    //@TODO : add Rage Probs
                }
            }
        }
    }


    /**
     * This Method will Read all Strings with the old Method from Inibin v.1
     *
     * @param byte $iFormat
     * @access private
     */
    private function ReadStringFromInibin($iFormat)
    {
        if (($iFormat & 0x1000) == 0)
        {
            $this->debug("Kein Offset Segment");
        } else
        {
            $this->debug("String Offset Position: " . $this->getStreamPointer());
            $iLastOffset = -1;
            $arrKeys = $this->ReadSegmentKeys();


            $iOldStyleOffset = $this->getStreamPointer() + count($arrKeys) * 2;
            if (is_array($arrKeys))
            {
                foreach ($arrKeys AS $iKey)
                {
                    $iOffset = $this->ReadShort();
                    //$this->debug("String offset(".$iKey.") " . $iOffset);
                    $strVal = $this->getData($iOldStyleOffset + $iOffset);
                    $this->debug("String Value (".$iKey.") " . $strVal);

                    $this->arrKeyValue["".$iKey.""] = $strVal;    //php hack for keys higher than int32

                    $iLastOffset = $iOffset;
                }
            }
        }
    }


    /**
     * This method manages the whole parsing process
     * it will parse all values out of the *.inibin file
     *
     * @access private
     */
    private function ReadCharacterInibin()
    {
        // Header Info
        $iVersion = $this->decFile[0];
        $this->iStreamPointer++;


        $iOldLen = $this->ReadShort();
        $iFormat = $this->ReadShort();

        //u32 values
        $this->ReadU32FromInibin($iFormat);

        // float values
        $this->ReadFloatFromInibin($iFormat);

        // U8 values
        $this->ReadU8D10FromInibin($iFormat);

        // U16 values
        $this->ReadU16FromInibin($iFormat);

        // U8 values
        $this->ReadU8FromInibin($iFormat);

        // Boolean flags - single bit, ignoring
        $this->ReadBooleanFromInibin($iFormat);

        // 4-byte color values
        $this->Read4BColorFromInibin($iFormat);

        //rage values
        $this->ReadRageFromInibin($iFormat);

        //offsets to strings like inibin v.1
        $this->ReadStringFromInibin($iFormat);
    }


    /**
     * This Method reads the current Byte and returns it after the LittleEndian Method
     *
     * @return integer,float,double
     * @access private
     */
    private function ReadShort()
    {
        $bA = $this->getCurrentByte();
        $bB = $this->getCurrentByte();
        
        return (((0xff & $bB) << 8) | (0xff & $bA));
    }


    /**
     * This Method will return an Int32 Value
     * It will get the next 4-Bytes and returns a "LittleEndian" signed value
     *
     * @return integer,float,double
     * @access private
     */
    private function ReadInt32()
    {
        $bA = $this->hex8dec($this->getCurrentByte(true),true);
        $bB = $this->hex8dec($this->getCurrentByte(true),true);
        $bC = $this->hex8dec($this->getCurrentByte(true),true);
        $bD = $this->hex8dec($this->getCurrentByte(true),true);


        return (0xff & $bA) |
            ((0xff & $bB) << 8) |
            ((0xff & $bC) << 16) |
            ((0xff & $bD) << 24);
    }


    /**
     * This Method will read the next 4 Bytes (like Int32 Method)
     * and change them to an signed float value
     *
     * @return float
     * @access private
     */
    private function ReadFloat()
    {
        $arrReturn = unpack('f', pack('i', $this->ReadInt32()));
 
        return round($arrReturn[1],2);
    }


    /**
     * This Method will return the next Byte in Hex or Dec from the FileStream
     *
     * @param boolean $bHex returns the Hex Value
     * @return integer,hex(string)
     * @access private
     */
    private function getCurrentByte($bHex = false)
    {
        if ($bHex == false)
        {
            $strReturn = $this->decFile[$this->getStreamPointer()];
        } else
        {
            $strReturn = $this->hexFile[$this->getStreamPointer()];
        }

        $this->iStreamPointer++;;

        return $strReturn;
    }


    /**
     * This Method will read the count of the next segment and all their keys
     *
     * @return array Array of the Segment Keys
     * @access private
     */
    private function ReadSegmentKeys()
    {
        $iCount = $this->ReadShort();

        $this->debug("segment key count " . $iCount);

        if ($iCount == -1)
        {
            return null;
        }

        $result = array();
        for ($i = 0; $i < $iCount; $i++)
        {
           $result[$i] = $this->ReadInt32();
           //$this->debug("key[" + $i + "] " . $result[$i]);
        }

        return $result;
    }


    /**
     * This Method will return the current position in the FileStream
     *
     * @return integer
     * @access private
     */
    private function getStreamPointer()
    {
        //echo "Pointer: " . $this->iStreamPointer . "<br/>";
        return $this->iStreamPointer;
    }
}
?>